Skip to main content

Rich Text API

field.richText 富文本 API​

通过 FormDialogBuilder 添加的 RichText 富文本字段,可以在脚本中通过字段节点的 richText 属性读写富文本内容。

// 通过富文本的 text 字段名获取字段节点,再读取 richText
const richText = ctx.getFormContext().form.getField('contentTextStr').richText;

richText 帮你处理了富文本 3 个数据字段的一致性:

  • 读取:每次调用都基于 json 字段的最新内容计算,用户在编辑器中刚输入的内容也能立刻读到。
  • 写入:一次调用会同时更新 text / json / html 3 个字段,并触发一次 text 字段的 fieldOnChanged 事件。
  • 失败不落地:内容不合法或目标找不到时,3 个字段都保持原值,并抛出带错误码的异常。
  • 返回值是副本:所有方法返回的对象都是深拷贝,修改它们不会改变表单内容。
注意
  1. 只有富文本的 text 字段 才有 richText,普通字段以及 json / html 数据字段上 richText 为 undefined。建议使用可选链 ?. 访问。
  2. 字段节点的 value 始终是纯文本,请不要通过给 json / html 字段的 value 赋值来修改富文本。
  3. 图片 API 只引用已有图片(完整的 http/https 地址或平台上传后得到的 S3 key),不会上传文件;删除图片也只是从内容中移除引用,不会删除文件本身。

API 一览​

类型名称返回类型说明
方法get()RichTextSnapshot读取当前内容快照
方法set(content)RichTextSnapshot用新内容替换全文
方法append(content)RichTextSnapshot在末尾追加内容
方法prepend(content)RichTextSnapshot在开头插入内容
方法appendImage(image)RichTextSnapshot在末尾追加一张图片
方法clear()RichTextSnapshot清空内容
方法text(text, options)TextTarget按文字定位,用于修改、格式化、删除某段文字
方法block(index)BlockTarget按段落(块)定位
方法image(locator)ImageTarget按序号、地址或 S3 key 定位图片
方法edit(recipe)RichTextSnapshot批量修改,所有操作只提交一次
属性raw readonlyRawApi高级入口,直接操作 Tiptap JSON 节点

内容格式​

RichTextSnapshot 内容快照​

所有读写方法都返回最新的内容快照。

名称类型说明
documentobject富文本 JSON(Tiptap 格式),根节点 type 为 doc
textstring纯文本,和 text 字段的值一致
htmlobjectHTML 存储值:{ html: string, imageKeys: object, hasError: boolean, hasUploading: boolean }

ContentInput 块内容​

set、append、prepend 以及块定位的插入/替换方法,接收以下任意一种格式:

格式示例说明
字符串'第一段\n第二段'按换行拆分为多个段落
段落{ type: 'paragraph', content: InlineInput }一个段落
标题{ type: 'heading', level: 2, content: InlineInput }level 取值 1 ~ 6
图片{ type: 'image', image: ImageInput }一个只包含图片的段落
段落、标题、图片组成的数组[{ type: 'heading', level: 2, content: '标题' }, { type: 'paragraph', content: '正文' }]多个块;数组元素必须是块对象,不能是字符串

InlineInput 行内内容​

段落、标题的 content,以及文字定位的插入/替换方法,接收以下任意一种格式:

格式示例
字符串'普通文字'
带样式的文字{ text: '重点', bold: true, color: '#d4380d' }
以上格式组成的数组['合同金额:', { text: '100 万元', bold: true }]

带样式的文字支持以下属性:

名称类型说明
textstring文字内容,必填
boldboolean加粗
italicboolean斜体
underlineboolean下划线
strikeboolean删除线
colorstring文字颜色
linkstring超链接地址

ImageInput 图片​

url 和 s3Key 二选一。

名称类型说明
urlstring完整的 http/https 图片地址
s3Keystring平台上传完成后得到的 S3 key
altstring替代文字
titlestring图片标题
widthnumber宽度
heightnumber高度
align'left' | 'center' | 'right'对齐方式

读取内容​

get() 读取快照​

返回当前内容的 RichTextSnapshot。

const richText = ctx.getFormContext().form.getField('contentTextStr').richText;
const snapshot = richText.get();

console.log(snapshot.text); // 纯文本
console.log(snapshot.document); // 富文本 JSON
console.log(snapshot.html.html); // HTML 字符串

// 判断是否为空
if (!snapshot.text.trim()) {
qiqi.ui.message.showWarning('内容不能为空');
}

整体写入​

set(content) 替换全文​

用 ContentInput 替换全部内容。

// 纯文本,按换行拆分为两个段落
richText.set('第一段内容\n第二段内容');

// 标题 + 段落
richText.set([
{ type: 'heading', level: 2, content: '项目概况' },
{ type: 'paragraph', content: ['项目编号:', { text: 'P-2026-001', bold: true }] },
{ type: 'paragraph', content: '项目负责人:张三' },
]);

append(content) / prepend(content) 追加内容​

在末尾追加 / 在开头插入 ContentInput。如果当前内容为空,会直接替换掉空段落,不会留下空行。

// 末尾追加一段带链接的文字
richText.append({
type: 'paragraph',
content: ['详情请见:', { text: '项目主页', link: 'https://www.example.com/project/1' }],
});

// 开头插入一段红色加粗的提示
richText.prepend({
type: 'paragraph',
content: { text: '【草稿】', bold: true, color: '#cf1322' },
});

clear() 清空内容​

richText.clear();

按文字定位​

text(text, options) 文字定位​

按文字查找内容中的某一处,返回 TextTarget,再对这段文字做修改。

定位对象只记住查找条件,每次调用方法时都会在最新内容中重新查找,所以可以先保存定位对象,再在之后使用。

options 参数​

名称类型说明默认值
occurrencenumber | 'first' | 'last'有多处匹配时选第几处,数字从 0 开始'first'
caseSensitiveboolean是否区分大小写true
blocknumber | 'first' | 'last'只在指定段落(块)中查找,数字从 0 开始-

TextTarget 文字定位对象​

名称返回类型说明
get()object | undefined返回匹配位置信息(text、occurrence 等),找不到返回 undefined
exists()boolean是否找到
count()number匹配的总数(忽略 occurrence)
insertBefore(content)RichTextSnapshot在文字前插入 InlineInput
insertAfter(content)RichTextSnapshot在文字后插入 InlineInput
insertImageBefore(image)RichTextSnapshot在文字前插入图片
insertImageAfter(image)RichTextSnapshot在文字后插入图片
replaceWith(content)RichTextSnapshot用 InlineInput 替换这段文字
format(style)RichTextSnapshot设置样式,只修改声明的样式;color、link 传 null 表示移除
remove()RichTextSnapshot删除这段文字
block()BlockTarget定位到这段文字所在的段落
imageBefore()ImageTarget定位到紧挨在文字前面的图片
imageAfter()ImageTarget定位到紧挨在文字后面的图片
注意

get、exists、count 找不到时返回 undefined、false、0;修改类方法找不到时会抛出 TEXT_NOT_FOUND 错误。不确定文字是否存在时,请先用 exists() 判断。

语法​

const richText = ctx.getFormContext().form.getField('contentTextStr').richText;

// 把"待定"替换成实际日期,并加粗
richText.text('待定').replaceWith({ text: '2026-10-20', bold: true });

// 把所有"风险"加粗标红:先统计数量,再逐个处理
const total = richText.text('风险').count();
for (let i = 0; i < total; i++) {
richText.text('风险', { occurrence: i }).format({ bold: true, color: '#cf1322' });
}

// 去掉之前设置的颜色和链接
richText.text('项目主页').format({ color: null, link: null });

// 只在第 2 个段落中查找,并在后面补充文字
richText.text('负责人', { block: 1 }).insertAfter('(已确认)');

// 不区分大小写,删除最后一处
richText.text('todo', { caseSensitive: false, occurrence: 'last' }).remove();

// 先判断再修改,避免抛错
const target = richText.text('附:签名');
if (target.exists()) {
target.block().remove(); // 删除整段
}

按段落定位​

block(index) 段落定位​

按段落(块)定位,index 为数字(从 0 开始)、'first' 或 'last',返回 BlockTarget。

BlockTarget 段落定位对象​

名称返回类型说明
get()object | undefined返回段落信息(index、node),找不到返回 undefined
exists()boolean段落是否存在
insertBefore(content)RichTextSnapshot在段落前插入 ContentInput
insertAfter(content)RichTextSnapshot在段落后插入 ContentInput
insertImageBefore(image)RichTextSnapshot在段落前插入图片
insertImageAfter(image)RichTextSnapshot在段落后插入图片
replaceWith(content)RichTextSnapshot用 ContentInput 替换该段落
remove()RichTextSnapshot删除该段落;删除最后一个段落时会保留一个空段落
image(index)ImageTarget定位该段落中的第几张图片,数字从 0 开始

找不到段落时,修改类方法会抛出 BLOCK_NOT_FOUND 错误。

语法​

// 替换第一个段落为标题
richText.block('first').replaceWith({ type: 'heading', level: 1, content: '月度经营分析' });

// 在第 2 个段落后插入两段内容(顶层字符串按换行拆成多个段落)
richText.block(1).insertAfter('补充说明一\n补充说明二');

// 删除最后一个段落
if (richText.block('last').exists()) {
richText.block('last').remove();
}

// 在"结论"所在段落之前插入一个小标题
richText.text('结论').block().insertBefore({ type: 'heading', level: 3, content: '分析结论' });

图片​

appendImage(image) / image(locator) 图片​

  • appendImage(image):在末尾追加一个只包含图片的段落,参数为 ImageInput。
  • image(locator):定位一张图片,返回 ImageTarget。locator 可以是:
    • 数字(全文第几张,从 0 开始)、'first'、'last';
    • { url, occurrence?, block? }:按图片地址定位;
    • { s3Key, occurrence?, block? }:按 S3 key 定位。

ImageTarget 图片定位对象​

名称返回类型说明
get()object | undefined返回图片信息(index、source、node),找不到返回 undefined
exists()boolean图片是否存在
update(input)RichTextSnapshot修改图片属性,只修改声明的属性;属性传 null 表示清除
replace(image)RichTextSnapshot用新的 ImageInput 替换图片
remove()RichTextSnapshot从内容中移除图片(不删除文件)

找不到图片时,修改类方法会抛出 IMAGE_NOT_FOUND 错误。

语法​

// 追加一张网络图片,居中显示
richText.appendImage({
url: 'https://www.example.com/images/chart.png',
alt: '销售趋势图',
width: 480,
align: 'center',
});

// 追加一张已上传到平台的图片
richText.appendImage({ s3Key: 'tenant/rich-text/diagram.png', alt: '流程图' });

// 在"如下图所示:"之后插入图片
richText.text('如下图所示:').insertImageAfter({ url: 'https://www.example.com/images/flow.png' });

// 修改第一张图片的宽度并清除标题
richText.image('first').update({ width: 320, title: null });

// 把指定地址的图片替换为新图片
richText
.image({ url: 'https://www.example.com/images/old.png' })
.replace({ url: 'https://www.example.com/images/new.png', align: 'center' });

// 删除"签字:"后面紧挨着的签名图片
const signImage = richText.text('签字:').imageAfter();
if (signImage.exists()) {
signImage.remove();
}

批量修改​

edit(recipe) 批量修改​

edit 把多次修改合并为一次提交:在回调中调用的方法只会被记录,回调结束后按顺序执行,全部成功才写入表单,并且只触发一次 fieldOnChanged。任何一步失败,整批修改都不会生效。

回调参数 draft 提供和 richText 相同的写入方法:set、append、prepend、appendImage、clear、text(...)、block(...)、image(...),但没有 get、exists、count 等读取方法。

注意

回调必须是同步函数。传入 async 函数或返回 Promise 时会抛出 ASYNC_RECIPE_NOT_SUPPORTED 错误,表单内容不变。需要异步获取数据时,请先 await 拿到数据,再调用 edit。

语法​

const customerName = ctx.getFormContext().form.getField('customerName').value;

richText.edit((draft) => {
draft.clear();
draft.append({ type: 'heading', level: 2, content: '拜访记录' });
draft.append({ type: 'paragraph', content: ['客户:', { text: customerName || '-', bold: true }] });
draft.append('拜访结论:待补充');
// 后面的操作会基于前面操作后的内容执行
draft.text('待补充').format({ color: '#8c8c8c', italic: true });
});

高级:raw​

raw 用于直接操作富文本 JSON 节点,适合需要精确控制结构的场景。节点路径 path 是由 content 数组下标组成的数组:[] 表示根节点,[0] 表示第一个段落,[0, 1] 表示第一个段落中的第二个子节点。

名称返回类型说明
getSnapshot()RichTextSnapshot同 get()
read(path)object | undefined读取指定路径的节点,路径不存在返回 undefined
insert(input)RichTextSnapshot插入节点:{ parentPath?, index?, nodes },默认插入到根节点末尾
insertImage(input)RichTextSnapshot插入图片:{ source: { type: 'url' \| 's3Key', value }, parentPath?, index?, ... }
update(path, node)RichTextSnapshot整体替换指定路径的节点
remove(path)RichTextSnapshot删除指定路径的节点
replace(document)RichTextSnapshot用完整的 doc 替换全文
clear()RichTextSnapshot清空内容
apply(operations)RichTextSnapshot按顺序执行一组操作,全部成功才提交一次

语法​

const raw = richText.raw;

// 读取第一个段落节点
const firstBlock = raw.read([0]);

// 在根节点第 1 个位置插入一个分割线和一个段落
raw.insert({
index: 1,
nodes: [
{ type: 'horizontalRule' },
{ type: 'paragraph', content: [{ type: 'text', text: '以下为附件说明' }] },
],
});

// 一次执行多个操作;失败时可以通过 error.operationIndex 知道是第几个操作出错
raw.apply([
{ type: 'insertContent', position: 'append', content: '第一段' },
{ type: 'insertContent', position: 'append', content: '第二段' },
{ type: 'formatText', text: { text: '第二段' }, style: { bold: true } },
]);

// 用一份完整的 JSON 替换全文
raw.replace({
type: 'doc',
content: [
{ type: 'paragraph', content: [{ type: 'text', text: '全新的内容' }] },
],
});
注意

raw 写入的节点必须符合富文本编辑器支持的结构,否则会抛出 INVALID_DOCUMENT 或 UNSUPPORTED_NODE 错误。日常使用优先选择 set、append、text()、block() 等方法。

错误处理​

富文本 API 的异常都带有稳定的错误码 code,请根据 code 做判断,不要依赖错误文案。

属性类型说明
codestring错误码,见下表
messagestring错误描述
operationIndexnumber批量操作中失败的是第几个(从 0 开始)
pathnumber[]与错误相关的节点路径
错误码说明
TEXT_NOT_FOUND文字定位找不到
BLOCK_NOT_FOUND段落定位找不到
IMAGE_NOT_FOUND图片定位找不到
NODE_PATH_NOT_FOUNDraw 中的节点路径不存在
ASYNC_RECIPE_NOT_SUPPORTEDedit 传入了异步回调
INVALID_DOCUMENT内容结构不合法
UNSUPPORTED_NODE使用了编辑器不支持的节点
INVALID_IMAGE_SOURCE图片地址不是完整的 http/https 地址,或 S3 key 为空
INVALID_IMAGE_PARENT图片插入的位置不合法
SERIALIZE_FAILED生成纯文本或 HTML 失败
COMMIT_FAILED写入表单字段失败,3 个字段已恢复为原值
try {
richText.text('不存在的文字').remove();
} catch (e) {
if (e.code === 'TEXT_NOT_FOUND') {
qiqi.ui.message.showWarning('没有找到要删除的文字');
} else {
throw e;
}
}

完整示例​

下面的示例创建一个"周报"对话框:

  • 打开时写入周报模板;
  • 勾选"附加签名"时在末尾追加签名,取消勾选时删除签名段落;
  • 修改"项目名称"时同步替换正文中的项目名;
  • 提交时检查"待补充"占位文字是否都已填写,并读取结果。
const formDialogBuilder = qiqi.builder.createDialogBuilder();

const baseFieldSet = formDialogBuilder.addFieldSet({ title: '基本信息', fieldsPerLine: 2 });
baseFieldSet.addField({ id: 'projectName', title: '项目名称', type: 'Text' });
baseFieldSet.addField({ id: 'withSignature', title: '附加签名', type: 'Checkbox' });

const contentFieldSet = formDialogBuilder.addFieldSet({ title: '周报内容', fieldsPerLine: 1 });
contentFieldSet.addField({
id: 'content',
title: '周报内容',
type: 'RichText',
fieldNames: { text: 'contentTextStr', json: 'contentTextJson', html: 'contentTextHtml' },
});
contentFieldSet.addField({ id: 'contentTextStr', title: '周报内容', type: 'Text', hidden: true });
contentFieldSet.addField({ id: 'contentTextJson', title: '周报内容', type: 'JsonObject', hidden: true });
contentFieldSet.addField({ id: 'contentTextHtml', title: '周报内容', type: 'JsonObject', hidden: true });

const SIGNATURE_TEXT = '—— 本周报由系统生成';
// 记录上一次的项目名,用于替换正文
let lastProjectName = '【项目名称】';

function getRichText(ctx) {
return ctx.getFormContext().form.getField('contentTextStr').richText;
}

formDialogBuilder.open({
title: '填写周报',
size: 'large',

formOnLoad: (ctx) => {
getRichText(ctx).set([
{ type: 'heading', level: 2, content: ['项目:', { text: lastProjectName, bold: true }] },
{ type: 'heading', level: 3, content: '本周完成' },
{ type: 'paragraph', content: { text: '待补充', color: '#8c8c8c' } },
{ type: 'heading', level: 3, content: '下周计划' },
{ type: 'paragraph', content: { text: '待补充', color: '#8c8c8c' } },
{ type: 'heading', level: 3, content: '风险与问题' },
{ type: 'paragraph', content: '暂无' },
]);
},

fieldOnChanged: (ctx) => {
const { fieldName, currentField } = ctx.getCurrentContext();
const richText = getRichText(ctx);

// 写入富文本也会触发 contentTextStr 的 fieldOnChanged,只处理关心的字段,避免循环
if (fieldName === 'withSignature') {
const signature = richText.text(SIGNATURE_TEXT);
if (currentField.value && !signature.exists()) {
richText.append({ type: 'paragraph', content: { text: SIGNATURE_TEXT, italic: true } });
} else if (!currentField.value && signature.exists()) {
signature.block().remove();
}
return;
}

if (fieldName === 'projectName') {
const newName = currentField.value || '【项目名称】';
const target = richText.text(lastProjectName);
if (target.exists()) {
target.replaceWith({ text: newName, bold: true });
}
lastProjectName = newName;
}
},

dialogOnConfirm: async (ctx) => {
const richText = getRichText(ctx);
// 模板中的"待补充"还没有被替换,说明周报没有填写完整
if (richText.text('待补充').exists()) {
qiqi.ui.message.showError('请完善周报中"待补充"的内容');
ctx.getEventArgs().cancel();
return;
}

const snapshot = richText.get();
console.log('纯文本:', snapshot.text);
console.log('HTML:', snapshot.html.html);

const value = ctx.getFormContext().form.value;
// value.contentTextStr 纯文本
// value.contentTextJson 富文本 JSON
// value.contentTextHtml HTML 存储值
console.log(value);
},
});